package com.tchepannou.limbe.helper;

import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ClassHelper
{
    private static final Logger LOG = Logger.getLogger(ClassHelper.class.getName());

    public static List<Class<?>> loadClasses(ServletContext ctx, ClassHelper.Filter filter)
        throws ClassNotFoundException, IOException
    {
        /* all directories */
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        String path = ""; //packageName.replace('.', '/');
        Enumeration<URL> resources = classLoader.getResources(path);
        List<File> dirs = new ArrayList<File>();
        while (resources.hasMoreElements())
        {
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
        }

        /* Load the classes */
        ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
        for (File directory : dirs)
        {
            LOG.info("Directory: " + directory);
            List<Class<?>> tmp = loadClasses(directory, "", filter);
            LOG.info("Classes loaded from " + directory + ": " + tmp);

            classes.addAll(tmp);
        }

        /* Load classes from JARS */
        List<Class<?>> jars = loadClassesFromJar(ctx, filter);
        classes.addAll(jars);

        return classes;
    }

    private static List<Class<?>> loadClassesFromJar (ServletContext ctx, Filter filter)
            throws IOException, ClassNotFoundException
    {
        String path = ctx.getRealPath("WEB-INF/lib");
        LOG.info ("Jar directory: " + path);
        if (path == null)
        {
            return Collections.EMPTY_LIST;
        }

        List<Class<?>> result = new ArrayList<Class<?>>();
        File root = new File (path);
        File[] files = root.listFiles();
        if (files != null)
        {
            for (File file : files)
            {
                LOG.info("Jar: " + file);
                List<Class<?>> tmp = loadClassesFromJar(file, filter);
                LOG.info("Classes loaded from " + file + ": " + tmp);

                result.addAll(tmp);
            }
        }
        return result;
    }

    private static List<Class<?>> loadClassesFromJar(File file, Filter filter)
            throws IOException, ClassNotFoundException
    {
        String path = file.getAbsolutePath();
        JarFile jarFile = new JarFile(path);
        Enumeration entries = jarFile.entries();

        URL[] urls = { new URL("jar:file:" + path+"!/") };
        ClassLoader classLoader = URLClassLoader.newInstance(urls);
        List<Class<?>> result = new ArrayList<Class<?>>();

        while (entries.hasMoreElements())
        {
            JarEntry je = (JarEntry) entries.nextElement();
            if(je.isDirectory() || !je.getName().endsWith(".class")){
                continue;
            }
            // -6 because of .class
            String className = je.getName().substring(0,je.getName().length()-6);
            className = className.replace('/', '.');
            if (!isPackageExcluded(className))
            {
                try
                {
                    Class clazz = Class.forName(className);
                    if (filter.accept(clazz))
                    {
                        result.add(clazz);
                    }
                }
                catch (Throwable ex)
                {
                    LOG.log(Level.WARNING, "Unable to load " + className + " from " + file, ex);
                }
            }
        }
        return result;
    }

    private static final Set<String> EXCLUDED_PACKAGES;
    static{
        EXCLUDED_PACKAGES = new HashSet<String>();
        EXCLUDED_PACKAGES.add("java.");
        EXCLUDED_PACKAGES.add("javax.");
        EXCLUDED_PACKAGES.add("com.google.");
        EXCLUDED_PACKAGES.add("org.apache.");
        EXCLUDED_PACKAGES.add("com.sun.");
        EXCLUDED_PACKAGES.add("com.tchepannou.util.");
    }
    private static boolean isPackageExcluded(String className)
    {
        for (String prefix : EXCLUDED_PACKAGES)
        {
            if (className.startsWith(prefix))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Recursive method used to find all classes in a given directory and
     * subdirs.
     * 
     * @param directory
     *            The base directory
     * @param packageName
     *            The package name for classes found inside the base directory
     * @return The classes
     * @throws ClassNotFoundException
     */
    private static List<Class<?>> loadClasses(File directory, String packageName, Filter filter)
        throws ClassNotFoundException
    {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        if (!directory.exists())
        {
            return classes;
        }

        File[] files = directory.listFiles();
        for (File file : files)
        {
            //LOG.info(" File: " + file);
            if (file.isDirectory())
            {
                StringBuilder xpackageName = new StringBuilder();
                xpackageName.append(packageName);
                if (xpackageName.length() > 0)
                {
                    xpackageName.append('.');
                }
                xpackageName.append(file.getName());
                List<Class<?>> tmp = loadClasses(file, xpackageName.toString(), filter);
                classes.addAll(tmp);
            }
            else if (file.getName().endsWith(".class"))
            {
                String classname = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
                Class<?> clazz = Class.forName(classname);

                if (filter.accept(clazz))
                {
                    classes.add(clazz);
                }
            }

        }
        return classes;
    }

    // -- Static Classes
    public interface Filter
    {
        public boolean accept(Class<?> clazz);
    }
}
